<?php
namespace pmvc\annotation;

use pmvc\log\Log;
use pmvc\util\ClassUtil;
use pmvc\util\ReflectionUtil;

use stdClass;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
use ReflectionFunction;

/**
 * Default implementation of the {@link AnnotationReader} interface
 * that uses the {@link AnnotationParser} to parse annotations.
 *
 */
class AnnotationReaderImpl
	implements AnnotationReader {

	const ANNOTATION_INTERFACE = '\pmvc\annotation\Annotation';

	private $strict = false;

	/**
	 * Sets wheter or not the reader should be strict.  When
	 * the reader is strict it requires Annotation classes to
	 * implement the Annotation interface.  In non-strict mode
	 * annotations classes that have a {@code setParameters}
	 * method will be called and passed the parameters.
	 * @param bool $strict
	 */
	public function setStrict($strict=true) {
		$this->strict = $strict;
	}

	/**
	 * {@inheritDoc}
	 */
	function readClassAnnotations($class, Array $types=Array()) {
		// get ReflectionClass
		if (is_string($class)) {
			$class = new ReflectionClass($class);
		}

		// read and return the annotations
		return $this->getAnnotations($class->getDocComment(), $types);
	}

	/**
	 * {@inheritDoc}
	 */
	function readPropertyAnnotations($property, Array $types=Array()) {
		// get ReflectionClass
		if (is_array($property)) {
			$class = new ReflectionClass($property[0]);
			$property = $class->getProperty($property[1]);
		}

		// read and return the annotations
		return $this->getAnnotations($property->getDocComment(), $types);
	}

	/**
	 * {@inheritDoc}
	 */
	function readMethodAnnotations($method, Array $types=Array()) {
		// get ReflectionClass
		if (is_array($method)) {
			$class = new ReflectionClass($method[0]);
			$method = $class->getMethod($method[1]);
		}

		// read and return the annotations
		return $this->getAnnotations($method->getDocComment(), $types);
	}

	/**
	 * {@inheritDoc}
	 */
	function readFunctionAnnotations($function, Array $types=Array()) {
		// get ReflectionClass
		if (!ReflectionUtil::isInstance($function, 'ReflectionFunction')) {
			$function = new ReflectionFunction($function);
		}

		// read and return the annotations
		return $this->getAnnotations($function->getDocComment(), $types);
	}

	/**
	 * Reads, filters and creates the annotations from the given {@code $data}.
	 * @param string $data
	 */
	private function getAnnotations($data, $types=Array()) {
		$parserAnnotations = $this->readAnnotations($data);
		$parserAnnotations = $this->filterAnnotations($parserAnnotations, $types);
		return $this->createAnnotations($parserAnnotations);
	}

	/**
	 * Creates proper annotation objects from the given array
	 * of parsed annotation objects.
	 * @param array $parserAnnotations
	 */
	private function createAnnotations(Array $parserAnnotations) {
		$ret = Array();
		foreach ($parserAnnotations as $parserAnnotation) {
			$ret []= $this->createAnnotation($parserAnnotation);
		}
		return $ret;
	}

	/**
	 * Creates a propper annotation object from the given
	 * parsed annotation object.
	 * @param stdClass $annotation
	 */
	private function createAnnotation(stdClass $annotation) {

		// get class and vars
		$className = $annotation->name;
		$vars = isset($annotation->vars) && !empty($annotation->vars)
			?  $annotation->vars : Array();

		// load the class
		$clazz = new ReflectionClass($className);
		$isAnnot= $clazz->implementsInterface(self::ANNOTATION_INTERFACE);
		$hasParmeterMethod = $clazz->hasMethod("setParameters");

		// check some things
		if (!$isAnnot) {
			$msg = "Annotation ".$className." does not implement ".self::ANNOTATION_INTERFACE;
			if ($this->strict) {
				throw new Exception($msg);
			} else {
				Log::warn(__CLASS__, $msg);
			}
		}

		// create the class
		$annotation = $clazz->newInstance();

		// if it has the method, set the params
		if ($hasParmeterMethod) {
			$params = Array();
			foreach ($vars as $name=>$var) {
				$params[$name] = $this->processVar($var);
			}
			$annotation->setParameters($params);

		} else {
			Log::warn(__CLASS__, $className." does not have a setParameters method");
		}

		// return it
		return $annotation;
	}

	/**
	 * Processes a variable defined on an annotation.
	 * @param mixed $value
	 */
	private function processVar($value) {

		// process array
		if (is_array($value)) {
			$ret = Array();
			foreach ($value as $var) {
				$ret []= $this->processVar($var);
			}
			return $ret;

		// process annotation
		} else if (is_object($value)) {
			return $this->createAnnotation($value);

		// anything else
		} else {
			return $value;
		}
	}

	/**
	 * Filters the annotations of the given types.
	 *sss
	 * @param array $parserAnnotations
	 * @param array $types
	 */
	private function filterAnnotations(Array $parserAnnotations, Array $types) {
		// short circuit
		if (count($types)==0) {
			return $parserAnnotations;
		}

		// filter them
		$ret = Array();
		foreach ($parserAnnotations as $annotation) {
			foreach ($types as $type) {
				if (ReflectionUtil::classIsA($annotation->name, $type)) {
					$ret []= $annotation;
				}
			}
		}

		// return it
		return $ret;
	}

	/**
	 * Reads the annotations from the given data.
	 * @param string $data
	 */
	private function readAnnotations($data) {

		// create parser
		$parser = new AnnotationParser($data);

		// read annotations
		$ret = Array();
		while (($annot=$parser->nextAnnotation())!==false) {
			$ret []= $annot;
		}

		// return them
		return $ret;
	}

}

?>